package icerts

import (
	"context"
	"crypto"
	"crypto/tls"
	"crypto/x509"
	"encoding/pem"
	"fmt"
	"net/http"
	"time"

	"gitee.com/djaf/my-certs/app/classes/accounts"
	"gitee.com/djaf/my-certs/app/classes/certificates"
	"gitee.com/djaf/my-certs/app/classes/domains"
	"gitee.com/djaf/my-certs/app/classes/keypairs"
	"gitee.com/djaf/my-certs/app/web/dto"
	"github.com/caddyserver/certmagic"
	"github.com/libdns/libdns"
	"github.com/mholt/acmez"
	"github.com/mholt/acmez/acme"
	"github.com/starter-go/base/lang"
	"github.com/starter-go/vlog"
	"go.uber.org/zap"
)

type csrRunner struct {

	// input params
	context context.Context
	csr     *dto.CSR

	serviceCerts    certificates.Service
	serviceCSR      certificates.CSRService
	serviceAccounts accounts.Service
	serviceDomains  domains.Service
	serviceKeyPairs keypairs.Service

	// temp vars

	objectAccount     *dto.Account
	objectDomain      *dto.Domain
	objectCertificate *dto.Certificate

	domainNames   []string
	keyForAccount crypto.Signer
	keyForCert    crypto.Signer

	// result
}

func (inst *csrRunner) run() {

	steps := make([]func() error, 0)

	steps = append(steps, inst.checkParams)
	steps = append(steps, inst.loadAccount)
	steps = append(steps, inst.loadDomain)
	steps = append(steps, inst.prepareCertAndKey)
	steps = append(steps, inst.prepareDomainNameList)
	steps = append(steps, inst.loadAccountKey)
	steps = append(steps, inst.loadCertKey)
	steps = append(steps, inst.startRequest)
	steps = append(steps, inst.closeCSR)

	err := inst.runSteps(steps)
	if err == nil {
		return
	}
	vlog.Error(err.Error())
}

func (inst *csrRunner) runSteps(steps []func() error) error {
	for _, step := range steps {
		err := step()
		if err != nil {
			return err
		}
	}
	return nil
}

func (inst *csrRunner) checkParams() error {
	return nil
}

func (inst *csrRunner) loadAccount() error {
	ctx := inst.context
	item, err := inst.serviceAccounts.GetCurrent(ctx)
	if err != nil {
		return err
	}
	inst.objectAccount = item
	return nil
}

func (inst *csrRunner) loadDomain() error {
	ctx := inst.context
	dn := inst.csr.DN
	item, err := inst.serviceDomains.FindByName(ctx, dn)
	if err != nil {
		return err
	}
	inst.objectDomain = item
	return nil
}

func (inst *csrRunner) prepareDomainNameList() error {
	dn := string(inst.objectDomain.Name)
	if dn == "" {
		return fmt.Errorf("no domain name")
	}
	inst.domainNames = []string{dn}
	return nil
}

func (inst *csrRunner) prepareCertAndKey() error {

	ctx := inst.context
	cert := new(dto.Certificate)
	kp := new(dto.KeyPair)
	kp, err := inst.serviceKeyPairs.CreateNew(ctx, kp)
	if err != nil {
		return err
	}
	cert.KeyPair = kp.ID
	inst.objectCertificate = cert
	return nil
}

func (inst *csrRunner) loadAccountKey() error {
	ctx := inst.context
	kid := inst.objectAccount.KeyPair
	signer, err := inst.serviceKeyPairs.LoadSigner(ctx, kid)
	if err != nil {
		return err
	}
	inst.keyForAccount = signer
	return nil
}

func (inst *csrRunner) loadCertKey() error {
	ctx := inst.context
	kid := inst.objectCertificate.KeyPair
	signer, err := inst.serviceKeyPairs.LoadSigner(ctx, kid)
	if err != nil {
		return err
	}
	inst.keyForCert = signer
	return nil
}

func (inst *csrRunner) startRequest() error {

	ctx := inst.context
	url := inst.objectAccount.ServiceURL
	debug := inst.objectAccount.DebugMode
	email := inst.objectAccount.Email
	dnlist := inst.domainNames
	keyAccount := inst.keyForAccount
	keyCert := inst.keyForCert

	// logger
	logger, err := zap.NewDevelopment()
	if err != nil {
		return err
	}

	// dns solver
	solver := &certmagic.DNS01Solver{
		DNSProvider: &myDNSProvider{parent: inst},
	}

	// client
	skipVerify := false
	if debug {
		skipVerify = true
	}
	client := acmez.Client{
		Client: &acme.Client{
			Directory: url,
			HTTPClient: &http.Client{
				Transport: &http.Transport{
					TLSClientConfig: &tls.Config{
						InsecureSkipVerify: skipVerify,
					},
				},
			},
			Logger: logger,
		},
		ChallengeSolvers: map[string]acmez.Solver{
			// acme.ChallengeTypeHTTP01:    mySolver{},
			// acme.ChallengeTypeTLSALPN01: mySolver{},
			acme.ChallengeTypeDNS01: solver,
		},
	}

	// account
	account := acme.Account{
		Contact:              []string{"mailto:" + email},
		TermsOfServiceAgreed: true,
		PrivateKey:           keyAccount,
	}
	if debug {
		account, err = client.NewAccount(ctx, account)
		if err != nil {
			return err
		}
	}

	// send
	certs, err := client.ObtainCertificate(ctx, account, keyCert, dnlist)
	if err != nil {
		return fmt.Errorf("obtaining certificate: %v", err)
	}

	return inst.handleCerts(certs)
}

func (inst *csrRunner) closeCSR() error {
	return nil
}

func (inst *csrRunner) handleCerts(certs []acme.Certificate) error {
	for _, cert := range certs {
		vlog.Warn("handle cert: %s", cert.URL)
		err := inst.saveCert(&cert)
		if err != nil {
			return err
		}
	}
	return nil
}

func (inst *csrRunner) saveCert(src *acme.Certificate) error {

	ctx := inst.context
	data := src.ChainPEM
	kp := inst.objectCertificate.KeyPair
	dn := inst.csr.DN

	cert, err := inst.parseX509Cert(src)
	if err != nil {
		return err
	}

	dst := new(dto.Certificate)
	dst.DN = dn
	dst.KeyPair = kp
	dst.Content = lang.Base64FromBytes(data)
	dst.CommonName = cert.Subject.CommonName
	dst.NotAfter = lang.NewTime(cert.NotAfter)
	dst.NotBefore = lang.NewTime(cert.NotBefore)

	_, err = inst.serviceCerts.Insert(ctx, dst)
	return err
}

func (inst *csrRunner) parseX509Cert(src *acme.Certificate) (*x509.Certificate, error) {
	data := src.ChainPEM
	var result *x509.Certificate
	for {
		block, dataRest := pem.Decode(data)
		if block == nil {
			break
		}
		data = dataRest
		der := block.Bytes
		cert, err := x509.ParseCertificate(der)
		if err != nil {
			return nil, err
		}
		if result == nil {
			result = cert
		}
		t1 := result.NotBefore
		t2 := cert.NotBefore
		if t2.After(t1) {
			result = cert
		}
	}
	if result == nil {
		return nil, fmt.Errorf("the result cert is nil")
	}
	return result, nil
}

////////////////////////////////////////////////////////////////////////////////

type myDNSProvider struct {
	parent *csrRunner
}

func (inst *myDNSProvider) _impl() certmagic.ACMEDNSProvider {
	return inst
}

func (inst *myDNSProvider) AppendRecords(ctx context.Context, zone string, recs []libdns.Record) ([]libdns.Record, error) {

	csrService := inst.parent.serviceCSR
	csr1 := inst.parent.csr
	csr2 := new(dto.CSR)
	*csr2 = *csr1

	for _, rec := range recs {
		// start challenge
		challenge, err := inst.makeChallenge(ctx, zone, rec)
		if err != nil {
			return nil, err
		}
		challenge.Timeout = 10 * 60 * 1000 // 10 min
		csr2.Challenge = challenge
		err = csrService.UpdateState(ctx, csr2)
		if err != nil {
			return nil, err
		}
		// wait for challenge
		err = inst.waitForChallenge(ctx, csr2)
		if err != nil {
			return nil, err
		}
	}
	return recs, nil
}

func (inst *myDNSProvider) waitForChallenge(ctx context.Context, csr *dto.CSR) error {
	id := csr.ID
	csrService := inst.parent.serviceCSR
	now := lang.Now()
	deadline := now + lang.Time(csr.Challenge.Timeout)
	for {
		if deadline < now {
			return fmt.Errorf("wait for DNS challenge: timeout")
		}
		csr.ID = id
		err := csrService.GetState(ctx, csr)
		if err != nil {
			return err
		}
		if csr.Cancelled {
			break
		}
		challenge := csr.Challenge
		if challenge == nil {
			break
		}
		if challenge.Cancelled {
			break
		}
		if challenge.Done {
			break
		}
		time.Sleep(time.Second)
		now = lang.Now()
	}
	return nil
}

func (inst *myDNSProvider) DeleteRecords(ctx context.Context, zone string, recs []libdns.Record) ([]libdns.Record, error) {
	for _, rec := range recs {
		o, err := inst.makeChallenge(ctx, zone, rec)
		if err == nil {
			vlog.Warn("remove DNS rec : %s", o.Name)
		}
	}
	return recs, nil
}

func (inst *myDNSProvider) makeChallenge(ctx context.Context, zone string, rec libdns.Record) (*dto.ACMEChallenge, error) {
	dst := new(dto.ACMEChallenge)
	dst.Todo = fmt.Sprintf("resolve DNS %s record", rec.Type)
	dst.Name = rec.Name + "..." + zone
	dst.Value = rec.Value
	return dst, nil
}

////////////////////////////////////////////////////////////////////////////////
